﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using HQS.TokenInterpolation.Tokens;
namespace HQS.TokenInterpolation.Core
{
    internal class Scanner
    {
        private const string ErrorMultipleOpenParenthesis = "There are multiple opening ( for template \"{0}\".";
        private const string ErrorMultipleCloseParenthesis = "There are multiple closing ) for template \"{0}\".";
        private const string ErrorNoOpenParenthesis = "There is no opening ( for template template \"{0}\".";
        private const string ErrorNoCloseParenthesis = "There is no closing ) for template template \"{0}\".";
        private const string ErrorTemplateBodyMissing = "Template body is not found for template \"{0}\".";
        private const string ErrorTemplateNameMissing = "Template name is missing at \"{0}\".";
        private const string ErrorTemplateNotFound = "Template \"{0}\" is not found.";

        private const string ErrorNoOpeningCurlyBracket = "There is no opening curly bracket {{ at \"{0}\".";
        private const string ErrorMultipleOpenCurlyBrackets = "There is multiple curly brackets {{ without closing curly brackets.";
        private const string ErrorTokenWithNoName = "Token is not recognized with context \"{0}\".";
        private const string ErrorInvalidTokenContext = "Token \"{0}\" has an invalid context. {1}";

        internal const string EscapeRegExpPattern = @"\\[{}']*";
        internal const char LeftCurlyBracket = '{';
        internal const char RightCulryBracket = '}';
        internal const char OpenParenthesis = '(';
        internal const char CloseParenthesis = ')';
        internal const char ContextSeperator = ':';
        internal const string Template = "@template";

        private static readonly TokenCache tokenCache = new TokenCache();
        private static readonly TokenMetadata defaultTokenMetadata;

        private readonly List<Line> _lines = new List<Line>();
        private readonly HashSet<int> _escapedCurlyBracketsPositions = new HashSet<int>();
        private readonly List<IndexInfo> _indices = new List<IndexInfo>();
        private readonly List<Template> _templates = new List<Template>();
        private readonly List<IVisitable> _tokens = new List<IVisitable>();

        static Scanner()
        {
            foreach (var type in AppDomain.CurrentDomain.GetAssemblies().SelectMany(f => f.GetTypes().Where(t => Attribute.IsDefined(t, typeof(TokenAttribute))
                                                                                                                  && t.IsSubclassOf(typeof(AbstractToken)))))
            {
                TokenAttribute tokenAttribute =
                    type.GetCustomAttributes(typeof(TokenAttribute), false).First() as TokenAttribute;
                TokenMetadata metadata = new TokenMetadata();
                metadata.TokenType = type;
                metadata.Name = tokenAttribute.Name;
                metadata.Properties = new List<TokenContextProperty>();
                foreach (var prop in type.GetProperties())
                {
                    TokenPropertyAttribute attr =
    prop.GetCustomAttributes(typeof(TokenPropertyAttribute), true).FirstOrDefault() as TokenPropertyAttribute;
                    if (attr != null)
                    {
                        metadata.Properties.Add(new TokenContextProperty() { Property = prop, TokenContext = attr });

                    }
                }

                tokenCache[metadata.Name] = metadata;
            }

            defaultTokenMetadata = new TokenMetadata();
            defaultTokenMetadata.Properties = new List<TokenContextProperty>();
            foreach (var prop in typeof(DefaultToken).GetProperties())
            {
                TokenPropertyAttribute attr =
prop.GetCustomAttributes(typeof(TokenPropertyAttribute), false).FirstOrDefault() as TokenPropertyAttribute;

                if (attr != null)
                {
                    defaultTokenMetadata.Properties.Add(new TokenContextProperty() { Property = prop, TokenContext = attr });
                }
            }
        }

        public Scanner(string template)
        {
            this.OriginalText = template;
            this.BuildLines();
            this.BuildEscapedCurlyBracketsPositions();
            this.BuildIndices();
            this.BuildTemplates();
            this.BuildTokens();
        }

        public IList<Template> Templates
        {
            get { return _templates.AsReadOnly(); }
        }

        public IList<IVisitable> Tokens
        {
            get { return _tokens.AsReadOnly(); }
        }

        public string SanitizeText()
        {
            System.Text.StringBuilder sb = new StringBuilder(this.OriginalText);
            foreach (var template in this.Templates)
            {
                sb.Replace(template.OriginalText, string.Empty);
            }

            if (this._escapedCurlyBracketsPositions.Count > 0)
            {
                sb.Replace("\\{", "{");
                sb.Replace("\\}", "}");
            }

            return sb.ToString();
        }

        public string OriginalText { get; set; }

        private void BuildLines()
        {
            StringReader rdr = new StringReader(this.OriginalText);
            int lineNumber = 0;
            string line;
            int countLineFeed = Environment.NewLine.ToCharArray().Length;
            while ((line = rdr.ReadLine()) != null)
            {
                lineNumber++;
                int start = lineNumber == 1 ? 0 : _lines.Last().End + 1;
                _lines.Add(new Line(lineNumber, start, start + line.Length + countLineFeed - 1));
            }
        }

        private void BuildEscapedCurlyBracketsPositions()
        {
            foreach (Match match in new Regex(EscapeRegExpPattern).Matches(this.OriginalText))
            {
                _escapedCurlyBracketsPositions.Add(match.Index);
            }
        }

        private void BuildIndices()
        {
            var scanner = new Stack<int>();
            for (int i = 0; i < this.OriginalText.Length; i++)
            {
                if (this.OriginalText[i] == LeftCurlyBracket && (_escapedCurlyBracketsPositions.Contains(i - 1) == false))
                {
                    scanner.Push(i);
                }
                else if (this.OriginalText[i] == RightCulryBracket && (_escapedCurlyBracketsPositions.Contains(i - 1) == false))
                {
                    if (scanner.Count == 0)
                    {
                        throw this.CreateSyntaxException(ErrorNoOpeningCurlyBracket, i, StringUtil.Between(this.OriginalText, i > 25 ? i - 25 : 0, i));
                    }

                    int index = scanner.Pop();
                    _indices.Add(new IndexInfo(index, i));
                }
            }

            if (scanner.Count != 0)
            {
                List<int> positions = new List<int>();
                while (scanner.Any())
                {
                    positions.Add(scanner.Pop());
                }

                throw this.CreateSyntaxException(ErrorMultipleOpenCurlyBrackets, positions);
            }
        }

        private SyntaxException CreateSyntaxException(string message, int index, params string[] extraParameters)
        {
            return this.CreateSyntaxException(message, new List<int> { index }, extraParameters);
        }

        private SyntaxException CreateSyntaxException(string message, List<int> indices, params object[] extraParameters)
        {
            var lines = indices.Select(index => _lines.First(f => index >= f.Start && index <= f.End)).ToList();

            var text = string.Join(" ", lines.Select(f => this.OriginalText.Substring(f.Start, f.End - f.Start - 1)));
            SyntaxException exception = new SyntaxException(string.Format(message, extraParameters),
                                      string.Join(",", lines.Select(f => f.Number.ToString())),
                                      string.Join(",", lines.Select((f, i) => f.End - indices.ElementAt(i))));

            exception.OffendingText = text;

            System.Diagnostics.Trace.TraceError(exception.ToString() + "\r\n" + exception.OffendingText);
            return exception;
        }

        private void BuildTemplates()
        {
            int idxTemplate = this.OriginalText.IndexOf(Template, StringComparison.OrdinalIgnoreCase);
            int closeParenthesisIndex = 0;
            while (idxTemplate > 0)
            {
                bool foundOpen = false;
                bool foundClose = false;
                System.Text.StringBuilder sb = new StringBuilder();
                IndexInfo indexInfo = null;
                for (int i = idxTemplate + Template.Length; i < this.OriginalText.Length; i++)
                {
                    char c = this.OriginalText[i];
                    if (c == Scanner.OpenParenthesis)
                    {
                        if (foundOpen)
                        {
                            throw this.CreateSyntaxException(ErrorMultipleOpenParenthesis, idxTemplate, StringUtil.Between(this.OriginalText, idxTemplate, i));
                        }

                        foundOpen = true;
                    }
                    else if (c == Scanner.CloseParenthesis)
                    {
                        if (foundClose)
                        {
                            throw this.CreateSyntaxException(ErrorMultipleCloseParenthesis, idxTemplate, StringUtil.Between(this.OriginalText, idxTemplate, i));
                        }
                        closeParenthesisIndex = i;
                        foundClose = true;
                    }
                    else if (c == Scanner.LeftCurlyBracket)
                    {
                        string afterClose = StringUtil.Between(this.OriginalText, closeParenthesisIndex + 1, i - 1);
                        if (afterClose.All(f => f == '\n' || f == '\r' || f == ' '))
                        {
                            indexInfo = _indices.First(f => f.Start == i);
                        }

                        break;
                    }
                    else
                    {
                        if (foundOpen)
                        {
                            sb.Append(c);
                        }
                        else
                        {
                            if (Char.IsWhiteSpace(c) == false)
                            {
                                break; // this means it is not right. "we found @template but the first character is not (
                            }
                        }
                    }
                }

                if (foundOpen && foundClose && indexInfo != null)
                {
                    string templateContent = StringUtil.Between(this.OriginalText, indexInfo.Start + 1, indexInfo.End - 1);
                    List<KeyPairContext> contextList = this.ParseContextProperties(sb.ToString());
                    var nameKeyPairContext =
                        contextList.FirstOrDefault(f => f.Key.Equals("name", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(f.Value) == false);

                    if (nameKeyPairContext != null)
                    {
                        var template = new Template()
                        {
                            Name = nameKeyPairContext.Value,
                            Context = sb.ToString(),
                            StartIndex = idxTemplate,
                            EndIndex = indexInfo.End,
                            Content = templateContent,
                            OriginalText = StringUtil.Between(this.OriginalText, idxTemplate, indexInfo.End)

                        };

                        _templates.Add(template);
                        _indices.Remove(indexInfo);
                    }
                    else
                    {
                        throw this.CreateSyntaxException(ErrorTemplateNameMissing, idxTemplate,
                            StringUtil.Between(this.OriginalText, idxTemplate,
                                this.OriginalText.IndexOf(Scanner.CloseParenthesis, idxTemplate)));
                    }
                }
                else
                {
                    int endIndex = idxTemplate + 25 > this.OriginalText.Length ? this.OriginalText.Length - idxTemplate - 1 : idxTemplate + 25;

                    if (foundOpen == false)
                    {
                        throw this.CreateSyntaxException(ErrorNoOpenParenthesis, idxTemplate, StringUtil.Between(this.OriginalText, idxTemplate, endIndex));
                    }

                    if (foundClose == false)
                    {
                        throw this.CreateSyntaxException(ErrorNoCloseParenthesis, idxTemplate, StringUtil.Between(this.OriginalText, idxTemplate, endIndex));
                    }

                    endIndex = this.OriginalText.IndexOf(Scanner.CloseParenthesis, idxTemplate);
                    throw this.CreateSyntaxException(ErrorTemplateBodyMissing, idxTemplate, StringUtil.Between(this.OriginalText, idxTemplate, endIndex));
                }

                idxTemplate = this.OriginalText.IndexOf(Template, indexInfo.End, StringComparison.OrdinalIgnoreCase);
            }
        }

        private void BuildTokens()
        {
            foreach (var indexInfo in _indices.OrderBy(f => f.Start))
            {
                if (_tokens.Any(f => f.EndIndex > indexInfo.Start && f.EndIndex > indexInfo.End) == false)
                {
                    AbstractToken token = null;
                    string originalText = StringUtil.Between(this.OriginalText, indexInfo.Start, indexInfo.End);
                    string context = StringUtil.Between(originalText, originalText.IndexOf(Scanner.LeftCurlyBracket) + 1, originalText.LastIndexOf(Scanner.RightCulryBracket) - 1).Trim();

                    token = NewToken(indexInfo.Start, context);

                    IVisitableTemplate templateToken = token as IVisitableTemplate;
                    if (templateToken != null)
                    {
                        templateToken.Template = _templates.FirstOrDefault(f => (templateToken.TemplateName.Equals(f.Name)));
                        if (templateToken.Template == null)
                        {
                            throw this.CreateSyntaxException(ErrorTemplateNotFound, indexInfo.Start,
                              templateToken.TemplateName);
                        }
                    }

                    token.OriginalText = originalText;
                    token.StartIndex = indexInfo.Start;
                    token.EndIndex = indexInfo.End;
                    if (string.IsNullOrWhiteSpace(token.Name))
                    {
                        throw this.CreateSyntaxException(ErrorTokenWithNoName, indexInfo.Start, token.OriginalText); ;
                    }

                    _tokens.Add(token);
                }
            }
        }

        private AbstractToken NewToken(int index, string context)
        {
            List<KeyPairContext> contextList = this.ParseContextProperties(context);
            Type tokenType = null;
            KeyPairContext invokeKeyPairContext =
                contextList.FirstOrDefault(
                    f => f.IsColonFound && f.Key.Equals("invoke", StringComparison.OrdinalIgnoreCase));

            string tokenName = null;
            TokenMetadata metadata = null;
            if (invokeKeyPairContext == null)
            {
                // here we try to see if the token starts with the token name like {foreach .....} instead of {invoke:foreach....}
                var firstMatch = contextList.Where(f => f.Key == f.Value).ToList();
                if (firstMatch.Count > 0)
                {
                    foreach (var kvp in firstMatch)
                    {
                        if (tokenCache.TryGetValue(kvp.Key, out metadata))
                        {
                            tokenName = metadata.Name;
                            tokenType = metadata.TokenType;
                            break;
                        }
                    }
                }

                if (tokenType == null && contextList.Count > 0)
                {
                    tokenName = contextList.First().Key;
                    tokenType = typeof(DefaultToken);
                    metadata = defaultTokenMetadata;
                }
            }
            else
            {
                tokenName = invokeKeyPairContext.Value;
                var entry = tokenCache.FirstOrDefault(f => f.Key.Equals(tokenName, StringComparison.OrdinalIgnoreCase));
                if (default(KeyValuePair<string, TokenMetadata>).Equals(entry) == false)
                {
                    metadata = entry.Value;
                    tokenType = entry.Value.TokenType;
                }
            }

            if (tokenType != null)
            {
                AbstractToken token = Activator.CreateInstance(tokenType) as AbstractToken;
                string errorMessage = null;
                if (token.AutoBind == false)
                {
                    token.Init();
                    errorMessage = token.ValidateContext();
                }
                else
                {
                    token.Init();
                    errorMessage = this.BindProperties(contextList, metadata, token);
                }

                if (string.IsNullOrWhiteSpace(errorMessage) == false)
                {
                    throw this.CreateSyntaxException(ErrorInvalidTokenContext, index, tokenName, errorMessage);
                }

                token.Name = tokenName.Trim();
                return token;
            }

            throw this.CreateSyntaxException(ErrorTokenWithNoName, index, context);
        }

        [DebuggerDisplay("{Value} {IsColonFound}")]
        class KeyPairContext
        {
            public string Key;
            public string Value;
            public bool IsColonFound;
        }

        private string BindProperties(List<KeyPairContext> contextProperties, TokenMetadata metadata, AbstractToken token)
        {
            Dictionary<string, string> reviseDictionary = this.ReviseContext(contextProperties, metadata);
            foreach (var prop in metadata.Properties)
            {
                string value;
                if (reviseDictionary.TryGetValue(prop.TokenContext.Name, out value))
                {
                    if (prop.Property.PropertyType == typeof(string))
                    {
                        prop.Property.SetValue(token, value, new object[]{});
                    }
                    else if (prop.Property.PropertyType == typeof(int))
                    {
                        int intValue;
                        if (int.TryParse(value, out intValue))
                        {
                            prop.Property.SetValue(token, intValue, new object[] { });
                        }
                    }
                    else if (prop.Property.PropertyType == typeof(bool))
                    {
                        bool boolValue = StringUtil.ToBoolean(value);
                        prop.Property.SetValue(token, boolValue, new object[] { });
                    }
                    else if (prop.Property.PropertyType == typeof(char))
                    {
                        prop.Property.SetValue(token, value.Trim().FirstOrDefault(), new object[] { });
                    }
                }
            }

            List<string> errors = new List<string>();
            foreach (var prop in metadata.Properties.Where(f => f.TokenContext.Required))
            {
                string value;
                reviseDictionary.TryGetValue(prop.TokenContext.Name, out value);
                if (string.IsNullOrWhiteSpace(value))
                {
                    errors.Add(prop.TokenContext.Name);
                }
            }

            if (errors.Count > 0)
            {
                if (errors.Count == 1)
                {
                    return "This token is missing a required property \"" + errors.First() + "\".";
                }

                return "This token is missing required properties \"" + string.Join(",", errors) + "\".";
            }

            return null;

        }

        private Dictionary<string, string> ReviseContext(List<KeyPairContext> source, TokenMetadata metadata)
        {
            Dictionary<string, string> revisedSource = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            HashSet<string> hash = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            metadata.Properties.Select(f => f.TokenContext.Name)
                               .ToList()
                               .ForEach(f => hash.Add(f));


            foreach (var kvp in source)
            {
                if (kvp.IsColonFound)
                {
                    if (hash.Contains(kvp.Key))
                    {
                        revisedSource[kvp.Key] = kvp.Value;
                    }
                    else
                    {
                        Debug.WriteLine("Ignoring " + kvp.Key + ":" + kvp.Value);
                    }
                }
                else
                {
                    if (hash.Contains(kvp.Key) || revisedSource.Count == 0)
                    {
                        revisedSource[kvp.Key] = kvp.Value;
                    }
                    else if (kvp.IsColonFound == false)
                    {
                        var kvpLast = revisedSource.Last();
                        revisedSource[kvpLast.Key] = kvpLast.Value + " " + kvp.Value;
                    }
                    else
                    {
                        Debug.WriteLine("Ignoring " + kvp.Key + ":" + kvp.Value);
                    }
                }
            }

            return revisedSource;
        }

        private List<KeyPairContext> ParseContextProperties(string data)
        {
            bool colonFound = false;
            System.Text.StringBuilder normalized = new StringBuilder();
            for (int i = 0; i < data.Length; i++)
            {
                char c = data[i];
                if (c == ':')
                {
                    colonFound = true;
                }

                if (Char.IsWhiteSpace(c) && colonFound)
                {
                }
                else
                {
                    if (c != ':')
                    {
                        colonFound = false;
                    }

                    normalized.Append(c);
                }
            }

            List<string> pairs = new List<string>();
            System.Text.StringBuilder buffer = new StringBuilder();
            foreach (var entry in normalized.ToString().Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
            {
                buffer.Append(entry);
                string bufferStr = buffer.ToString();

                HashSet<int> esc = new HashSet<int>();
                foreach (Match match in new Regex(EscapeRegExpPattern).Matches(bufferStr))
                {
                    esc.Add(match.Index);
                }

                int countOpen = 0;
                int countClosed = 0;
                int idx = bufferStr.IndexOf('{', 0);
                while (idx >= 0)
                {
                    if (esc.Contains(idx) == false)
                    {
                        countOpen++;
                    }

                    idx = bufferStr.IndexOf('{', idx + 1);
                }

                idx = bufferStr.IndexOf('}', 0);
                while (idx > 0)
                {
                    if (esc.Contains(idx) == false)
                    {
                        countClosed++;
                    }

                    idx = bufferStr.IndexOf('}', idx + 1);
                }

                if (countOpen == countClosed)
                {
                    pairs.Add(buffer.ToString());
                    buffer.Length = 0;
                }
                else
                {
                    buffer.Append(' ');
                }
            }

            //Add Token then strip it so we can ignore unused properties
            List<KeyPairContext> contextProperteis = new List<KeyPairContext>();
            foreach (string pair in pairs)
            {
                int idxColon = pair.IndexOf(':');
                KeyPairContext context = new KeyPairContext();
                contextProperteis.Add(context);
                if (idxColon < 0)
                {
                    context.Key = pair;
                    context.Value = pair;
                }
                else
                {
                    context.Key = pair.Substring(0, idxColon).Trim();
                    context.Value = pair.Substring(idxColon + 1).Trim();
                    context.IsColonFound = true;
                }
            }

            return contextProperteis;
        }
    }
}
